﻿using System;
using System.Collections.Generic;
using AzukiMap.Tools;
using AzukiMap.Tools.Logger;
using GeoAPI.CoordinateSystems;
using GeoAPI.Geometries;
using ProjNet.CoordinateSystems;

namespace AzukiMap.Map
{
    public class MapCoordinate
    {
        #region fields

        private readonly GeometryUtil _geomUtil;
        private double _adjX;
        private double _adjY;
        private double _angle;
        private CoordinationUnits _coordUnit;
        private IPolygon _currentExtent;
        private int _flipY;
        private IPoint _geoCenter;
        private double _geoScale;
        private double _inchesPerUnit;
        private IPolygon _lastExtent;
        private double _maxScale;
        private double _minScale;
        private IPolygon _renderedExtent;
        private double _screenHeight;

        private double _screenScale;
        private double _screenWidth;

        DatelineConnection _datelineConnection = new DatelineConnection();

        private int _srid;

        #endregion

        #region properties

        public IPolygon CurrentExtent
        {
            get { return _currentExtent; }
        }

        public IPolygon LastExtent
        {
            get { return _lastExtent; }
        }

        public IPolygon RenderedExtent
        {
            get { return _renderedExtent; }
        }

        public double ScreenScale
        {
            get { return _screenScale; }
            set
            {
                _screenScale = value;
                SetParamsByCenter();
            }
        }

        public double GeoScale
        {
            get { return _geoScale; }
        }

        public IPoint GeoCenter
        {
            get { return _geoCenter; }
        }

        public double ScreenWidth
        {
            get { return _screenWidth; }
        }

        public double ScreenHeight
        {
            get { return _screenHeight; }
        }

        public int Srid
        {
            set
            {
                if (value > 0)
                {
                    _srid = value;
                    SetInchesPerUnit(value);
                }
                else
                {
                    _srid = 4326;
                }
                _currentExtent.SRID = _srid;
            }
            get { return _srid; }
        }

        public CoordinationUnits MapUnit
        {
            get { return _coordUnit; }
        }

        public double Angle
        {
            set
            {
                _angle = value;
                SetParamsByCenter();
            }
            get { return _angle; }
        }

        public double AdjX
        {
            get { return _adjX; }
            set { _adjX = value; }
        }

        public double AdjY
        {
            get { return _adjY; }
            set { _adjY = value; }
        }

        public bool FlipY
        {
            get { return (_flipY == 1); }
            set { _flipY = (value) ? 1 : -1; }
        }

        public double Tolerance
        {
            get { return _screenScale/2; }
        }

        public double InchesPerUnit
        {
            get { return _inchesPerUnit; }
        }

        internal double MinScale
        {
            get { return _minScale; }
            set
            {
                if (value < 0)
                {
                    Logger.Error("Layer.minScale: MinScale should be >= 0.");
                    throw new ArgumentException("MinScale should be >= 0.", "MinScale");
                }
                if (_maxScale != 0 && _maxScale < value)
                {
                    Logger.Error("Layer.minScale: MinScale should be less than MinScale.");
                    throw new ArgumentException("MinScale should be less than MinScale.", "MinScale");
                }
                _minScale = value;
            }
        }

        internal double MaxScale
        {
            get { return _maxScale; }
            set
            {
                if (value <= 0)
                {
                    Logger.Error("Layer.MaxScale: MaxScale should be > 0.");
                    throw new ArgumentException("MaxScale should be > 0.", "MaxScale");
                }
                if (_minScale != 0 && _minScale > value)
                {
                    Logger.Error("Layer.MaxScale: MaxScale should be greater than MinScale.");
                    throw new ArgumentException("MaxScale should be greater than MinScale.", "MinScale");
                }
                _maxScale = value;
            }
        }

        private bool _is180Connected = false;



        internal bool Is180Connected
        {
            get { return _is180Connected; }
            set
            {
                _is180Connected = value;
                _datelineConnection.Is180Connected = value;
            }
        }

        private double _westEnd;

        internal double WestEnd
        {
            get { return _westEnd; }
            set
            {
                _westEnd = value;
                _datelineConnection.WestEnd = value;
            }
        }

        private double _eastEnd;

        internal double EastEnd
        {
            get { return _eastEnd; }
            set
            {
                _eastEnd = value;
                _datelineConnection.EastEnd = value;
            }
        }

        #endregion

        # region constructor

        public MapCoordinate()
        {
            _geomUtil = GeometryUtil.Default;
            initParams(4326);
        }

        public MapCoordinate(int srid)
        {
            _geomUtil = GeometryUtil.GetBySRID(srid);
            initParams(srid);
        }

        private void initParams(int srid)
        {
            //_currentExtent = _geomUtil.CreateExtentPolygon(0, 0, 0, 0, 0, 0, 0, 0);
            _currentExtent = _geomUtil.CreateExtentPolygon(0, 0, 0, 0);
            _currentExtent.SRID = srid;
            //_lastExtent = _geomUtil.CreateExtentPolygon(0, 0, 0, 0, 0, 0, 0, 0);
            //_renderedExtent = _geomUtil.CreateExtentPolygon(0, 0, 0, 0, 0, 0, 0, 0);
            _lastExtent = null;
            _renderedExtent = null;
            _flipY = 1;
            _screenWidth = AzukiMapSettings.INIT_DUMMY_SCREEN_SIZE;
            _screenHeight = AzukiMapSettings.INIT_DUMMY_SCREEN_SIZE;
            _screenScale = 1.0;
            _angle = 0.0;
            _srid = srid;
            // TODO: get map unit from SRID.
            _coordUnit = CoordinationUnits.DEGREES;

            SetInchesPerUnit(srid);
            _geoCenter = _geomUtil.CreatePoint(0, 0);
            _geoCenter.SRID = srid;

            SetParamsByCenter();
        }

        # endregion

        private void SetInchesPerUnit(int srid)
        {
            ICoordinateSystem cs = SridParser.GetCoordinateSystemFromSrid(srid);
            var csInfo = (Info) cs.GetUnits(0);
            if (csInfo.GetType() == typeof (LinearUnit))
            {
                double metersPerUnit = ((LinearUnit) csInfo).MetersPerUnit;
                _inchesPerUnit = metersPerUnit*AzukiMapSettings.INCHES_PER_METER;
            }
            if (csInfo.GetType() == typeof (AngularUnit))
            {
                //double radiansPerUnit = ((AngularUnit) csInfo).RadiansPerUnit;
                _inchesPerUnit = AzukiMapSettings.INCHES_PER_DEGREES;
            }
        }

        #region public methods

        public IPoint GetGeoPointFromPixel(double x, double y)
        {
            ICoordinate topLeft = _geomUtil.GetExtentPoint(_currentExtent, ExtentOrder.TOP_LEFT);

            double pixDist = GetPixDistanceFromScreenOrigin(x, y);
            double geoDist = pixDist*_screenScale;
            double angle2Tl = GetAngleFromScreenOrigin(x, y);

            double totalAngle = angle2Tl - _angle;
            double lon = geoDist*Math.Cos(totalAngle) + topLeft.X;
            double lat = geoDist*Math.Sin(totalAngle)*_flipY + topLeft.Y;

            IPoint geoPoint = _geomUtil.CreatePoint(lon, lat);

            return geoPoint;
        }

        public IPoint GetPixPointFromGeoOnScreen(double lon, double lat)
        {
            return GetPixPointFromGeo(lon, lat, 0, 0);
        }

        public IPoint GetPixPointFromGeoOnScreen(IPoint geoPoint)
        {
            return GetPixPointFromGeo(geoPoint.X, geoPoint.Y, 0, 0);
        }

        public IPoint GetPixPointFromGeo(double lon, double lat)
        {
            return GetPixPointFromGeo(lon, lat, _adjX, _adjY);
        }

        public IPoint GetPixPointFromGeo(IPoint geoPoint)
        {
            return GetPixPointFromGeo(geoPoint.X, geoPoint.Y, _adjX, _adjY);
        }

        public void MoveByPixel(double moveX, double moveY)
        {
            //double centerLon = _geoCenter.X + moveX * _screenScale;
            //double centerLat = _geoCenter.Y + moveY * _screenScale;
            double centerLon = _geoCenter.X + moveX*_screenScale;
            double centerLat = _geoCenter.Y + moveY*_screenScale*_flipY*-1;

            Logger.Debug(
                "MapCoordinate.MoveByPixel: "
                + " moveX = " + moveX + " "
                + " moveY = " + moveY + " "
                + " centerLon = " + centerLon + " "
                + " centerLat = " + centerLat + " "
                + " _screenScale = " + _screenScale
                + " _adjX = " + _adjX
                + " _adjY = " + _adjY
                );

            MoveTo(centerLon, centerLat);
        }

        public void MoveTo(double centerLon, double centerLat)
        {
            double dLength = GetDiagonalLength();
            double dAngle = GetDiagonalAngle();

            double tlAngle = Math.PI - dAngle - _angle;
            double blAngle = Math.PI + dAngle - _angle;
            double brAngle = 2*Math.PI - dAngle - _angle;
            double trAngle = dAngle - _angle;

            double tlLon = centerLon + dLength/2*Math.Cos(tlAngle);
            double blLon = centerLon + dLength/2*Math.Cos(blAngle);
            double brLon = centerLon + dLength/2*Math.Cos(brAngle);
            double trLon = centerLon + dLength/2*Math.Cos(trAngle);

            double tlLat = centerLat + dLength/2*Math.Sin(tlAngle)*_flipY;
            double blLat = centerLat + dLength/2*Math.Sin(blAngle)*_flipY;
            double brLat = centerLat + dLength/2*Math.Sin(brAngle)*_flipY;
            double trLat = centerLat + dLength/2*Math.Sin(trAngle)*_flipY;

            _geoCenter = _geomUtil.CreatePoint(centerLon, centerLat);
            _geoCenter.SRID = _srid;

            double exWidth = _geomUtil.GetPolygonEnvelopeWidth(_currentExtent);
            if (exWidth != 0)
            {
                SetPreviousExtents();
            }
            _currentExtent = _geomUtil.CreateExtentPolygon(tlLon, tlLat, blLon, blLat, brLon, brLat, trLon, trLat);
            _currentExtent.SRID = _srid;

            Logger.Debug(
                "MapCoordinate.SetCenter(2): "
                + " centerLon = " + centerLon
                + " centerLat = " + centerLat
                + " _geoCenter = " + _geoCenter
                + " dLength = " + dLength
                + " dAngle = " + dAngle
                + " _currentExtent = " + _currentExtent
                );
        }

        public void MoveTo(double centerLon, double centerLat, double screenScale)
        {
            _screenScale = screenScale;
            SetGeoScaleByScreenScale(_screenScale);

            Logger.Debug(
                "MapCoordinate.SetCenter(3): "
                + " centerLon = " + centerLon
                + " centerLat = " + centerLat
                + " screenScale = " + screenScale
                );

            MoveTo(centerLon, centerLat);
            ClearPreviousExtents();
        }

        public void MoveToScale(double centerLon, double centerLat, double geoScale)
        {
            _geoScale = geoScale;
            //SetGeoScaleByScreenScale(_screenScale);
            SetScreenScaleByGeoScale(geoScale);

            Logger.Debug(
                "MapCoordinate.MoveToScale(): "
                + " centerLon = " + centerLon
                + " centerLat = " + centerLat
                + " screenScale = " + geoScale
                );

            MoveTo(centerLon, centerLat);
            ClearPreviousExtents();
        }

        public void MoveTo(double centerLon, double centerLat, double screenScale, double angle)
        {
            _angle = angle;

            Logger.Debug(
                "MapCoordinate.SetCenter(4): "
                + " centerLon = " + centerLon
                + " centerLat = " + centerLat
                + " screenScale = " + screenScale
                + " angle = " + angle
                );

            MoveTo(centerLon, centerLat, screenScale);
        }

        public IPolygon MoveToExtent(IPolygon extent)
        {
            IEnvelope envelope = extent.EnvelopeInternal;
            return MoveToExtent(envelope.MinX, envelope.MinY, envelope.MaxX, envelope.MaxY);
        }

        /// <summary>
        /// Set the map extent by 2 points.
        /// When the map is rotated(angle is not 0), the map extent will be a polygon that contains the 2 points
        /// on the polygon edges.
        /// </summary>
        /// <param name="minX"></param>
        /// <param name="minY"></param>
        /// <param name="maxX"></param>
        /// <param name="maxY"></param>
        /// TODO: add min+maxscale controls.
        /// <returns></returns>
        public IPolygon MoveToExtent(double minLon, double minLat, double maxLon, double maxLat)
        {
            double verticalAngle = _angle*-1;
            double horizontalAngle = Math.PI/2 - _angle;

            // get Y-intersects of lines through the extent vertexes.
            double[] vYIntersects = GetYIntersectsFromExtent(minLon, minLat, maxLon, maxLat, verticalAngle);
            double vLowYIntersect = vYIntersects[0];
            double vHighYIntersect = vYIntersects[1];

            double[] hYIntersects = GetYIntersectsFromExtent(minLon, minLat, maxLon, maxLat, horizontalAngle);
            double hLowYIntersect = hYIntersects[0];
            double hHighYIntersect = hYIntersects[1];

            // get minimum extent with angle that can contain requested area.
            IPoint tl = GetIntersectionByAngles(horizontalAngle, verticalAngle, hHighYIntersect, vHighYIntersect);
            IPoint bl = GetIntersectionByAngles(horizontalAngle, verticalAngle, hHighYIntersect, vLowYIntersect);
            IPoint br = GetIntersectionByAngles(horizontalAngle, verticalAngle, hLowYIntersect, vLowYIntersect);
            IPoint tr = GetIntersectionByAngles(horizontalAngle, verticalAngle, hLowYIntersect, vHighYIntersect);

            // minimum extent width and screen scales.
            double minWidth = Math.Sqrt(Math.Pow(tl.X - tr.X, 2) + Math.Pow(tl.Y - tr.Y, 2));
            double minHeight = Math.Sqrt(Math.Pow(tl.X - bl.X, 2) + Math.Pow(tl.Y - bl.Y, 2));

            SetScreenScale(minWidth, minHeight);

            double centerLon = (minLon + maxLon)/2;
            double centerLat = (minLat + maxLat)/2;

            Logger.Debug(
                "MapCoordinate.SetExtent(): "
                + " minLon = " + minLon
                + " minLat = " + minLat
                + " maxLon = " + maxLon
                + " maxLat = " + maxLat
                + " centerLon = " + centerLon
                + " centerLat = " + centerLat
                );
            MoveTo(centerLon, centerLat);
            SetParamsByCenter();
            _renderedExtent = null;
            return _currentExtent;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// TODO: add min+maxscale controls.
        public void SetScreenSize(double width, double height)
        {
            double geoWidth = _geomUtil.GetExtentPolygonWidth(_currentExtent);
            double geoHeight = _geomUtil.GetExtentPolygonHeight(_currentExtent);
            //double scaleX = (geoHeight * Math.Sin(_angle) + geoWidth * Math.Cos(_angle)) / _screenWidth;
            //double scaleY = (geoHeight * Math.Cos(_angle) + geoWidth * Math.Sin(_angle)) / _screenHeight;
            double scaleX = geoWidth/width;
            double scaleY = geoHeight/height;

            _screenScale = (scaleX >= scaleY) ? scaleX : scaleY;
            _screenWidth = width;
            _screenHeight = height;
            SetGeoScaleByScreenScale(_screenScale);

            Logger.Debug(
                "MapCoordinate.SetScreenSize: "
                + " width = " + width
                + " height = " + height
                + " geoWidth = " + geoWidth
                + " geoHeight = " + geoHeight
                + " scaleX = " + scaleX
                + " scaleY = " + scaleY
                + " _screenScale = " + _screenScale
                );

            SetParamsByCenter();
            ClearPreviousExtents();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="zoomRatio"></param>
        /// <returns></returns>
        /// TODO: add min+maxscale controls.
        public double ZoomByRatio(double zoomRatio)
        {
            if (zoomRatio <= 0)
            {
                throw new ArgumentOutOfRangeException("zoomRatio should be > 0");
            }
            _screenScale *= zoomRatio;
            SetGeoScaleByScreenScale(_screenScale);

            Logger.Debug(
                "MapCoordinate.SetScreenSize: "
                + " zoomRatio = " + zoomRatio
                + " _screenScale = " + _screenScale
                );

            SetParamsByCenter();
            ClearPreviousExtents();
            return _screenScale;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="geoScale"></param>
        public void ZoomByScale(double geoScale)
        {
            if (geoScale <= 0)
            {
                throw new ArgumentOutOfRangeException("geoScale should be > 0");
            }
            if (geoScale < _minScale)
            {
                geoScale = _minScale;
            }
            if (_maxScale < geoScale)
            {
                geoScale = _maxScale;
            }
            _geoScale = geoScale;
            SetScreenScaleByGeoScale(geoScale);
            SetParamsByCenter();
            ClearPreviousExtents();
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="zoomRatio"></param>
        /// <param name="anchorX"></param>
        /// <param name="anchorY"></param>
        /// <returns></returns>
        /// TODO: add min+maxscale control
        public double ZoomByRatioAndAnchor(double zoomRatio, double anchorX, double anchorY)
        {
            if (zoomRatio < 0)
            {
                throw new ArgumentOutOfRangeException("zoomRatio should be > 0");
            }
            anchorX += _adjX;
            anchorY += _adjY;

            IPoint anchorPoint = GetGeoPointFromPixel(anchorX, anchorY);
            ICoordinate currentOrigin = _geomUtil.GetExtentPoint(_currentExtent, ExtentOrder.TOP_LEFT);

            double newScale = _screenScale*zoomRatio;

            double lx = anchorPoint.X - currentOrigin.X;
            double ly = anchorPoint.Y - currentOrigin.Y*_flipY;

            double originX = anchorPoint.X - lx*zoomRatio;
            double originY = anchorPoint.Y - ly*zoomRatio;
            double width = _screenWidth*newScale;
            double height = _screenHeight*newScale;

            double newCenterX = originX + width/2;
            if ( Is180Connected )
            {
                 newCenterX = _datelineConnection.GetRegulatedLon(newCenterX);
            }
            double newCenterY = originY - height/2*_flipY;

            _screenScale = newScale;
            SetGeoScaleByScreenScale(newScale);

            Logger.Debug(
                "MapCoordinate.ZoomByRatioAndPixel: "
                + " anchorX = " + anchorX
                + " anchorY= " + anchorY
                + " zoomRatio = " + zoomRatio
                + " newScale = " + newScale
                + " width = " + width
                + " height = " + height
                + " newCenterX = " + newCenterX
                + " newCenterY = " + newCenterY
                + " lx = " + lx
                + " ly = " + ly
                + " originX = " + originX
                + " originY = " + originY
                );
            
            MoveTo(newCenterX, newCenterY);
            ClearPreviousExtents();

            return _screenScale;
        }

        public ICoordinate GetExtentPoint(ExtentOrder position)
        {
            return _geomUtil.GetExtentPoint(_currentExtent, position);
        }

        internal void ClearLastExtent()
        {
            _lastExtent = null;
        }

        #endregion

        #region private methods

        private IPoint GetPixPointFromGeo(double lon, double lat, double adjX, double adjY)
        {
            double distFromCenter = GetDistanceFromCenter(lon, lat);
            double angle2Center = GetAngleToCenter(lon, lat);

            double totalAngle = angle2Center - _angle;

            double x = (distFromCenter/_screenScale)*Math.Cos(totalAngle) + _screenWidth/2 - adjX;
            double y = (distFromCenter/_screenScale)*Math.Sin(totalAngle) + _screenHeight/2 - adjY;

            IPoint pixPoint = _geomUtil.CreatePoint(x, y);
            return pixPoint;
        }

        private double GetAngleToCenter(double lon, double lat)
        {
            double distFromCenter = GetDistanceFromCenter(lon, lat);
            double angle2Center = 0;
            if (distFromCenter != 0)
            {
                angle2Center = Math.Asin((_geoCenter.Y - lat)*_flipY/distFromCenter);
                if (lon < _geoCenter.X)
                {
                    angle2Center = Math.PI - angle2Center;
                }
            }

            return angle2Center;
        }

        private double GetDistanceFromCenter(double lon, double lat)
        {
            return Math.Sqrt(Math.Pow((_geoCenter.X - lon), 2) + Math.Pow((_geoCenter.Y - lat), 2));
        }

        private double GetAngleFromScreenOrigin(double x, double y)
        {
            double a = 0;
            double r = GetPixDistanceFromScreenOrigin(x, y);
            if (r != 0)
            {
                a = Math.Asin(y*-1/r);
                if (x < 0)
                {
                    a = Math.PI - a;
                }
            }
            return a;
        }

        private double GetPixDistanceFromScreenOrigin(double x, double y)
        {
            return Math.Sqrt(Math.Pow(x, 2) + Math.Pow(y, 2));
        }

        /*
        internal void MoveByPixel(double moveX, double moveY, double adjX, double adjY)
        {
            _adjX = adjX;
            _adjY = adjY;
            MoveByPixel(moveX, moveY);
        }
         */

        private void SetPreviousExtents()
        {
            _lastExtent = _currentExtent;
            if (_renderedExtent == null)
            {
                _renderedExtent = _lastExtent;
            }
            else
            {
                // TODO: fix union method
                //_renderedExtent.Union(_lastExtent);
                _renderedExtent = _lastExtent;
            }
        }

        private void ClearPreviousExtents()
        {
            _lastExtent = null;
            _renderedExtent = null;
        }

        private void SetParamsByCenter()
        {
            Logger.Debug(
                "MapCoordinate.SetParamsByCenter: "
                );
            MoveTo(_geoCenter.X, _geoCenter.Y);
        }

        public IPoint GetCenter()
        {
            return _geomUtil.GetPolygonCenter(_currentExtent);
        }

        private void SetScreenScale(double geoWidth, double geoHeight)
        {
            double scaleX = geoWidth/_screenWidth;
            double scaleY = geoHeight/_screenHeight;
            _screenScale = (scaleX >= scaleY) ? scaleX : scaleY;
            SetGeoScaleByScreenScale(_screenScale);
        }

        private void SetGeoScaleByScreenScale(double screenScale)
        {
            //_geoScale = screenScale*AzukiMapSettings.DPI*AzukiMapSettings.InchesPerUnit(MapUnit);
            _geoScale = screenScale*AzukiMapSettings.DPI*_inchesPerUnit;
        }

        private void SetScreenScaleByGeoScale(double geoScale)
        {
            //_screenScale = geoScale/(AzukiMapSettings.DPI*AzukiMapSettings.InchesPerUnit(MapUnit));
            _screenScale = geoScale/(AzukiMapSettings.DPI*_inchesPerUnit);
        }


        private double[] GetYIntersectsFromExtent(double minx, double miny, double maxx, double maxy, double angle)
        {
            var yIntersects = new List<double>(4);
            yIntersects.Add(-Math.Tan(angle)*maxx + maxy);
            yIntersects.Add(-Math.Tan(angle)*maxx + miny);
            yIntersects.Add(-Math.Tan(angle)*minx + maxy);
            yIntersects.Add(-Math.Tan(angle)*minx + miny);

            yIntersects.Sort();

            double[] yIntersectsMinMax = {yIntersects[0], yIntersects[3]};

            return yIntersectsMinMax;
        }

        private IPoint GetIntersectionByAngles(double hAngle, double vAngle, double hYIntersect, double vYIntersect)
        {
            double x = (vYIntersect - hYIntersect)/(Math.Tan(hAngle) - Math.Tan(vAngle));
            double y = Math.Tan(vAngle)*x + vYIntersect;
            IPoint intersection = _geomUtil.CreatePoint(x, y);
            return intersection;
        }

        private IPolygon SetExtent(IPolygon extent)
        {
            throw new NotImplementedException();
        }

        private double GetDiagonalLength()
        {
            double geoWidth = _screenWidth*_screenScale;
            double geoHeight = _screenHeight*_screenScale;
            double diagonal = Math.Sqrt(Math.Pow(geoWidth, 2) + Math.Pow(geoHeight, 2));
            return diagonal;
        }

        private double GetDiagonalAngle()
        {
            double dAngle = Math.Atan(_screenHeight/_screenWidth);
            return dAngle;
        }

        #endregion
    }
}